﻿namespace Bowlus.Common
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.Linq;
    using System.ServiceModel;
    using Microsoft.WindowsAzure.ServiceRuntime;
    using System.Threading;

    /***************************************************************
     *** Example of how to use NotifyModule

     *** Define Server Side logic
        var nms = new NotifyModuleServer(delegate(string msg) {
            if (msg.Equals("AppHostSync"))
            {
                appHostManager.Sync();
            }
        });
        nms.startNotifyService();

     *** Send msg using the client   
        (new NotifyModuleClient()).NotifyAllInstancesInAnotherRole("AppHostSync");

     ****************************************************************/

    /*
     * This is the only service contract: Client notify Server a string msg. 
     * Fire and forget. Client doesn't get blocked.
     * Server take action based on the simple string msg. The actural server action is defined in the Handle delegate.
     */ 
    [ServiceContract(Namespace = "Bowlus.NotifyService")]
    public interface INotifyService
    {
        [OperationContract(IsOneWay = true)]
        void notify(string msg);
    }

    /*
     * Client side logic. 
     * Both Roles (FrontEnd/AppHost) can be the client. Both Roles can be the server.
     */
    public class NotifyModuleClient
    {
        // Bowlus.FrontEnd, Bowlus.AppHost
        private readonly string clientRoleName;

        // Bowlus.FrontEnd, Bowlus.AppHost
        private readonly string serverRoleName;

        // Use rolen name as endpoint name. Bowlus.FrontEnd, Bowlus.AppHost
        private readonly string serverEndpoint;

        private static readonly NetTcpBinding Binding = new NetTcpBinding(SecurityMode.None);

        private readonly ChannelFactory<INotifyService> factory = new ChannelFactory<INotifyService>(Binding);

        public NotifyModuleClient()
        {
            this.clientRoleName = RoleEnvironment.CurrentRoleInstance.Role.Name;
            this.serverRoleName = this.clientRoleName == BowlusSettings.FrontEndRoleName ? BowlusSettings.AppHostRoleName : BowlusSettings.FrontEndRoleName;
            this.serverEndpoint = this.serverRoleName;
        }

        // nofify all instances in another role
        public void NotifyAllInstancesInAnotherRole(string message)
        {
            var endPoints = RoleEnvironment.Roles[this.serverRoleName].Instances.Select(instance => instance.InstanceEndpoints[this.serverEndpoint]);

            foreach (var ep in endPoints)
            {
                var address = new EndpointAddress(
                    string.Format("net.tcp://{0}/{1}", ep.IPEndpoint, this.serverEndpoint));
                var client = this.factory.CreateChannel(address);

                try
                {
                    if (client != null)
                    {
                        Trace.TraceInformation(string.Format("Notify {0}/{1}: {2}", ep.IPEndpoint, this.serverEndpoint, message));

                        // Note: this method should not be blocked on the server side. See 'Server Side' logic.
                        client.notify(message);  
                        ((ICommunicationObject)client).Close();
                    }
                }
                catch (Exception ex)
                {
                    Trace.TraceError(ex.Message + '\n' + ex.StackTrace);
                    var communicationObject = (ICommunicationObject)client;
                    if (communicationObject != null)
                    {
                        communicationObject.Abort();
                    }
                }
            }
        }
    }

    /*
     * Server side logic.
     */

    // Defines the actual action taken, when the server is notified.
    public delegate void NotifyHandle(string msg);
    
    [ServiceBehavior(InstanceContextMode = InstanceContextMode.Single, ConcurrencyMode = ConcurrencyMode.Single)]
    public class NotifyModuleServer : INotifyService
    {
        public NotifyHandle TheNotifyNandle { get; set; }

        string serverEndpoint { set; get; }  //Use rolen name as endpoint name. Bowlus.FrontEnd, Bowlus.AppHost

        string ipServerEndpoint { set; get; }

        private Dictionary<string, Thread> workerThreads = new Dictionary<string, Thread>();

        int retries = 0;

        public NotifyModuleServer(NotifyHandle _callback)
        {
            this.TheNotifyNandle = _callback;

            var roleName = RoleEnvironment.CurrentRoleInstance.Role.Name;
            serverEndpoint = roleName;
            
            RoleInstanceEndpoint serviceEndPoint = RoleEnvironment.CurrentRoleInstance.InstanceEndpoints[serverEndpoint];
            ipServerEndpoint = serviceEndPoint.IPEndpoint.ToString();
        }

        // Note: this method shouldn't block. Server should return immediately, and do the actural work in another thread.
        // We deliberately choose to use our own thread instead of WCF thread, because we want to keep communication simple
        // by keeping only one WCF thread and only one service instance.
        public void notify(string msg)
        {
            if (workerThreads.ContainsKey(msg))
            {
                Trace.TraceInformation(string.Format("GOT Notification: {0}, however the same request has not finished processing. Request not processed.", msg));
                return;
            }

            Trace.TraceInformation(string.Format("GOT Notification: {0}", msg));

            Thread thread = new Thread(new ThreadStart(() => {
                try { TheNotifyNandle(msg); }
                catch (Exception ex) { Trace.TraceError(ex.Message + '\n' + ex.StackTrace); }                 
                workerThreads.Remove(msg); 
            }));

            workerThreads.Add(msg, thread);

            thread.Start();
        }

        public void startNotifyService()
        {
            ServiceHost serviceHost = new ServiceHost(this);

            serviceHost.Faulted += (sender, e) =>
            {
                Trace.TraceInformation(string.Format("HOST FAULT. Restarting the host. Retry {2}", retries++));
                serviceHost.Abort();
                startNotifyService();
            };

            NetTcpBinding binding = new NetTcpBinding(SecurityMode.None);

            serviceHost.AddServiceEndpoint(
               typeof(INotifyService),
               binding,
               String.Format("net.tcp://{0}/{1}", ipServerEndpoint, serverEndpoint));

            try
            {
                serviceHost.Open();
                Trace.TraceInformation(string.Format("Notify Server Started."));
            }
            catch (Exception ex)
            {
                Trace.TraceError(ex.Message + '\n' + ex.StackTrace);
            }
        }
        
    }

}
